package stv5;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;

import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;

import stv5.Request.Target;
import stv5.abstracted.Connection;
import stv5.abstracted.ConnectionAcceptor;
import stv5.abstracted.Database;
import stv5.abstracted.EpisodeManager;
import stv5.abstracted.RequestHandler;
import stv5.handlers.*;
import stv5.managers.LocalFileManager;
import stv5.managers.MediaTombManager;
import stv5.managers.TversityManager;
import us.danlib.net.Mysql;

/**
 * This is the main class for SeriesTracker v5. It accepts clients
 * 	and hands off their requests to appropriate RequestHandlers.
 * <br><br>
 * Calling without any args will start the server on port 8080,
 *  looking for TVersity on port 41952, and save the database
 *  in "stdb.xml". Otherwise, the arguments are:
 * <pre>
 * stv5 {-p port} 
 * 	{--local-player player}
 * 	{--compat arg1,arg2,...}
 * 	[-dm mysqldb,mysqluser,mysqlpass,{mysqlsvr} | -df {configfile}]
 * 	[-et {tvport} | -em {mtport} | -el folders] 
 * </pre>
 * Where:
 * <pre>
 * -p port - defines the port to broadcast on, if not 80
 * --local-player - specifies a path to the local player, if you don't
 * 		want to use the one in your settings database
 * --compat - specifies to use compatibility mode for your episode manager,
 * 		if available. This helps if you use different episode managers
 * 		in various settings. See documentation for your episode manager
 * 		for usage of compatibility mode
 * -dm - specifies using a mysqldb for the database, with relevant args 
 * 		separated by commas
 * 		the default svr is "localhost"
 * -df - specifies to use a flatfile config, using the file named "configfile"
 * 		the default file is "stdb.xml"
 * -et - specifies to use tversity to manage episodes, on port tvport
 * 		the default tversity port is 41952
 * -em - specifies to use mediatomb to manage episodes, on port mtport
 * 		the default mediatomb port is 49152
 * -el - specifies to use only local file storage to manage episodes,
 * 		with the location of series folders given in folders. This can
 * 		be either a colon (:) separated list of folders, or the name
 * 		of a file which has folder locations on separate lines
 * </pre>  
 * 
 * Potential new format:
 * <pre>
 * stv5 {-p, --port port}
 *  {-l, --local-player player}
 * 	{--compat arg1,arg2,...}
 *  [-d, --database (mysql/flat)]
 *  [-e, --episodes (tversity/mediatomb/local)]
 *  {--mysql-db dbname}
 *  {--mysql-user username}
 *  {--mysql-pass password}
 *  {--mysql-server servername}  
 *  {--db-file db_filename}
 * 
 *  {--tversity-port tvport} 
 *  {--mediatomb-port mtport}
 *  {--mediatomb-tr-profile profileName}
 *  {--mediatomb-tr-check [forceExtensions...]} 
 *  {--local-folders folders} 
 * </pre>
 * @author Daniel
 *
 */
public class SeriesTracker implements Runnable {
	public static Collection<String> asList(String...strings ) {
		LinkedList<String> ret = new LinkedList<String>();
		for (String s : strings)
			ret.add(s);
		return ret;
	}
	public static void main(String[] args) {

		// prepare the commandline parser
		OptionParser parser = new OptionParser();
		parser.acceptsAll( asList("?","h","help") , "show help");
		OptionSpec<Integer> portArg = parser.acceptsAll( asList("p", "port"), "SeriesTracker port" )			
			.withRequiredArg().ofType( Integer.class )
			.defaultsTo(8080)
			.describedAs("port");
		OptionSpec<String> playerPath = parser.acceptsAll( asList("l","local-player") )
			.withRequiredArg().ofType( String.class )
			.describedAs("player");
		/*
		OptionSpec<String> compatArgs = parser.accepts("compat", "compatibility mode: [arg1,arg2...]")
			.withOptionalArg().ofType( String.class )
			.withValuesSeparatedBy(',')
			.describedAs("arg1,arg2...");
		*/
		OptionSpec<String> dbtype = parser.acceptsAll( asList("d", "database"), "database type")
			.withRequiredArg().ofType( String.class )
			.describedAs("(mysql/flat)")
			.defaultsTo("flat");
		OptionSpec<String> emtype = parser.acceptsAll( asList("e", "episodes"), "episode manager")
			.withRequiredArg().ofType( String.class )
			.describedAs("(tversity/mediatomb/local)")
			.defaultsTo("tversity");
		OptionSpec<String> mysqldb = parser.accepts("mysql-db","mysql database name")
			.withRequiredArg().ofType( String.class )
			.defaultsTo("stv5");
		OptionSpec<String> mysqlserver = parser.accepts("mysql-server", "mysql server")
			.withRequiredArg().ofType( String.class )
			.defaultsTo("localhost");
		OptionSpec<String> mysqluser = parser.accepts("mysql-user", "mysql user")
			.withRequiredArg().ofType( String.class )
			.defaultsTo("stv5");
		OptionSpec<String> mysqlpass = parser.accepts("mysql-pass", "mysql password")
			.withRequiredArg().ofType( String.class )
			.defaultsTo("");
		OptionSpec<String> flatDbArg = parser.accepts("db-file", "(flat) database filename")
			.withRequiredArg().ofType( String.class )
			.defaultsTo("stv5.xml")
			.describedAs("xml database filename");
		OptionSpec<Integer> tvportArg = parser.accepts("tversity-port", "tversity's operating port")
			.withRequiredArg().ofType( Integer.class )
			.defaultsTo(41952);
		OptionSpec<Integer> mtportArg = parser.accepts("mediatomb-port", "mediatomb's operating port")
			.withOptionalArg().ofType( Integer.class )
			.defaultsTo(49152);
		OptionSpec<String> mtTrForce = parser.accepts("mediatomb-tr-check", 
				"If set, mediatomb will only transcode if a subtitle file is found, or is of " +
				"one of the supplied file extensions")
			.withOptionalArg()
			.describedAs("ext1,ext2,...")
			.withValuesSeparatedBy(',');
		OptionSpec<String> mtTrProfile = parser.accepts("mediatomb-tr-profile",
				"The profile to use for transcoding")
			.withRequiredArg().ofType( String.class )
			.defaultsTo("video-common");
		OptionSpec<String> localArg = parser.accepts("local-folders", "folders, separated" +
				" by \";\", or a filename with folders on separate lines")
			.withRequiredArg().ofType( String.class )
			.describedAs("folders");		
		OptionSet options = parser.parse(args);
		
		System.out.println("SeriesTracker v5 by Daniel Leong");
		if (options.has("?")) {
			try {
				parser.printHelpOn(System.out);
			} catch (IOException e) {
				e.printStackTrace();
			}
			System.exit(0);
		}
				
		EpisodeManager episodes = null;
		Database database = null;
		
		// start up the server so we can get status
		//	while it's loading
		int port = portArg.value( options );
		SeriesTracker stinstance = new SeriesTracker(port);
		stinstance.setRefreshTime(2);
		
		String dbString = dbtype.value(options);
		if ("mysql".equals(dbString)) {			
			// mysql db
			stinstance.setStatus("Connecting to DB...");
			String server = mysqlserver.value(options);
			String dbhost = mysqldb.value(options);
			String dbuser = mysqluser.value(options);
			String dbpass = mysqlpass.value(options);	
            System.out.print("* Connecting to DB... ");
			Mysql db = new Mysql(server, dbhost, dbuser, dbpass);
	        if (!db.connected()) {
	            System.out.println("Failed.");
	            System.exit(1);
	        }
	        System.out.println("Done.");
	        database = new MysqlDatabase(db);
		} else {
			// default to a flat file
			stinstance.setStatus("Loading database...");
			String cfgFile = flatDbArg.value(options);
			database = new FlatFileDatabase(cfgFile);
		}
		
		String emString = emtype.value(options);
		if ("local".equals(emString)) {
			// local file manager
			stinstance.setStatus("Waiting for file manager to finish processing...");
			episodes = new LocalFileManager(localArg.value(options));		
		} else if ("mediatomb".equals(emString)) {
			// mediatomb episode manager
			int mtport = mtportArg.value(options);
			boolean checkTr = options.has("mediatomb-tr-check");
			String trProf = mtTrProfile.value(options);
			
			stinstance.setStatus("Waiting for MediaTomb manager to finish processing...");
			episodes = new MediaTombManager(getBroadcastingIp(), 
					mtport, trProf, checkTr,
					mtTrForce.values(options)
			);			
		} else {
			// default to tversity episode manager
			int tvport = tvportArg.value(options);
			
			stinstance.setStatus("Waiting for Tversity manager to finish processing...");
			episodes = new TversityManager(getBroadcastingIp(), tvport);				
		}
		
		stinstance.setStatus("Preparing settings...");
		Settings thesettings = new Settings(episodes, database);
		if (options.has(playerPath))
			thesettings.setLocalPlayerLoc(playerPath.value(options));
				
		stinstance.setSettings(thesettings);
		System.out.println("Ready\n`````");
	}
			
	private HashMap<Request.Target, RequestHandler> handlers;
	private ConnectionAcceptor server;
	private boolean running = true;
	private Boolean ready = false;
	private StatusHandler status;
	private Thread thisThread;
		
	public SeriesTracker(int port) {
		handlers = new HashMap<Request.Target, RequestHandler>();
		status = new StatusHandler();
				
		initConnections(port);
				
		thisThread = new Thread(this);		
		thisThread.start();
		System.out.println("* Accepting connections port " + port);
	}
	
	/**
	 * Initialize the server to accept connections. Does not
	 * 	actually start accepting, though!
	 * 
	 * @param port The port to listen on
	 * @return True on successful init, else false on error
	 */
	private boolean initConnections(int port) {		
		boolean success = false;
		try {
			System.out.print("* Initializing connections... ");
			server = new STAcceptor(port);
			success = true;
			
			// prepare handlers that don't need Settings			
			handlers.put(Request.Target.FILES, new FileHandler());
		} catch (java.net.BindException e) {
			System.err.println("\nRequested port ("+port+") in use. Try another port.");
		} catch (IOException e) {
			e.printStackTrace();
		} 

		if (success) { 
			System.out.println("Done.");
		} else {
			System.err.println("Failed.");
			System.exit(1);
		} 
		
		return success;
	}	
	
	/**
	 * @see StatusHandler#setRefreshTime(int)
	 * @param newTime
	 */
	public void setRefreshTime(double newTime) {
		status.setRefreshTime(newTime);
	}

	/**
	 * Initializes the handlers which handle requests to the server.
	 * 
	 * @param An initialized Settings instance
	 */
	public void setSettings(Settings mySettings) {		
		handlers.put(Request.Target.INDEX, new IndexHandler(mySettings));		
		handlers.put(Request.Target.MEDIA, new MediaHandler(mySettings));
		handlers.put(Request.Target.SETTINGS, new SettingsHandler(mySettings));
		
		// we're ready to handle "actual" clients!
		synchronized(ready) {
			ready = true;
		}
	}
	
	public void setStatus(String newStatus) {
		status.setStatus(newStatus);
	}
	
	private synchronized void serverLoop() {
		Connection client = server.accept();
		
		Request req = client.read();
		
		synchronized(ready) {
			// if the result is null, it was a bad request
			if (req == null) 
				RequestHandler.handleError(client);
			else if (ready || req.getTarget() == Target.FILES)
				handleRequest(client, req);
			else
				// give it to the "status" handler
				status.handle(client, req);
		}
		
		// clean up
		client.flush();
		client.close();		
	}
	
	public void run() {
		while (thisThread == Thread.currentThread() && running) {
			serverLoop();
		}
	}
	
	private void handleRequest(Connection client, Request request) {
		if (handlers.containsKey(request.getTarget())) {
			request.setLocal( client.isLocal() );
			handlers.get(request.getTarget()).handle(client, request);
			//System.out.println("Served: " + request.getRaw());			
		}
		else RequestHandler.handleError(client);
	}
	

	/**
	 * @return The IP address this server is broadcasting on.
	 */
	public static String getBroadcastingIp() {
		String broadIp = null;
		try {
			broadIp = InetAddress.getLocalHost().getHostAddress();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
		return broadIp;
	}	
}
